<!DOCTYPE html>
<html>
<head>
  <title>System Dynamics</title>
  <meta name="description" content="A simple implementation of a system dynamics editor." />
  <!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
  <meta charset="UTF-8">
  <style>
    button {}
    .pointer_normal { font-weight: normal; background: #f0f0f0; }
    .pointer_selected { font-weight: bold; background: yellow; }
    .node_normal { font-weight: normal; background: #fff0f0; }
    .node_selected { font-weight: bold; background: #ff8080; }
    .link_normal { font-weight: normal; background: #f0fff0; }
    .link_selected { font-weight: bold; background: #80ff80; }
  </style>
  <script src="go.js"></script>
  <script src="../extensions/NodeLabelDraggingTool.js"></script>
  <link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" />  <!-- you don't need to use this -->
  <script src="goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    // SD is a global variable, to avoid polluting global namespace and to make the global
    // nature of the individual variables obvious.
    var SD = {
      mode: "pointer",   // Set to default mode.  Alternatives are "node" and "link", for
                         // adding a new node or a new link respectively.
      itemType: "pointer",    // Set when user clicks on a node or link button.
      nodeCounter: {stock:0, cloud:0, variable:0, valve:0}
    };
    var myDiagram;   // Declared as global

    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;

      myDiagram = $(go.Diagram, "myDiagram",
        {
          initialContentAlignment: go.Spot.Center,
          "undoManager.isEnabled": true,
          allowLink: false,  // linking is only started via buttons, not modelessly
          "animationManager.isEnabled": false,

          "linkingTool.portGravity": 0,  // no snapping while drawing new links
          "linkingTool.doActivate": function() {
            // change the curve of the LinkingTool.temporaryLink
            this.temporaryLink.curve = (SD.itemType === "flow") ? go.Link.Normal : go.Link.Bezier;
            this.temporaryLink.path.stroke = (SD.itemType === "flow") ? "blue" : "green";
            this.temporaryLink.path.strokeWidth = (SD.itemType === "flow") ? 5 : 1;
            go.LinkingTool.prototype.doActivate.call(this);
          },
          // override the link creation process
          "linkingTool.insertLink": function(fromnode, fromport, tonode, toport) {
            // to control what kind of Link is created,
            // change the LinkingTool.archetypeLinkData's category
            myDiagram.model.setCategoryForLinkData(this.archetypeLinkData, SD.itemType);
            // Whenever a new Link is drawng by the LinkingTool, it also adds a node data object
            // that acts as the label node for the link, to allow links to be drawn to/from the link.
            this.archetypeLabelNodeData = (SD.itemType === "flow") ? { category: "valve" } : null;
            // also change the text indicating the condition, which the user can edit
            this.archetypeLinkData.text = SD.itemType;
            return go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport);
          },

          "clickCreatingTool.archetypeNodeData": {},  // enable ClickCreatingTool
          "clickCreatingTool.isDoubleClick": false,   // operates on a single click in background
          "clickCreatingTool.canStart": function() {  // but only in "node" creation mode
            return SD.mode === "node" && go.ClickCreatingTool.prototype.canStart.call(this);
          },
          "clickCreatingTool.insertPart": function(loc) {  // customize the data for the new node
            SD.nodeCounter[SD.itemType] += 1;
            var newNodeId = SD.itemType + SD.nodeCounter[SD.itemType];
            this.archetypeNodeData = {
              key: newNodeId,
              category: SD.itemType,
              label: newNodeId
            };
            return go.ClickCreatingTool.prototype.insertPart.call(this, loc);
          }
        });

        // install the NodeLabelDraggingTool as a "mouse move" tool
        myDiagram.toolManager.mouseMoveTools.insertAt(0, new NodeLabelDraggingTool());

        // when the document is modified, add a "*" to the title and enable the "Save" button
        myDiagram.addDiagramListener("Modified", function(e) {
          var button = document.getElementById("SaveButton");
          if (button) button.disabled = !myDiagram.isModified;
          var idx = document.title.indexOf("*");
          if (myDiagram.isModified) {
            if (idx < 0) document.title += "*";
          } else {
            if (idx >= 0) document.title = document.title.substr(0, idx);
          }
        });

        // generate unique label for valve on newly-created flow link
        myDiagram.addDiagramListener("LinkDrawn", function(e) {
            var link = e.subject;
            if (link.category === "flow") {
                myDiagram.startTransaction("updateNode");
                SD.nodeCounter.valve += 1;
                var newNodeId = "flow" + SD.nodeCounter.valve;
                var labelNode = link.labelNodes.first();
                myDiagram.model.setDataProperty(labelNode.data, "label", newNodeId);
                myDiagram.commitTransaction("updateNode");
            }
        });

      buildTemplates();

      load();
    }

    function buildTemplates() {
      var $ = go.GraphObject.make;

      // helper functions for the templates
      function nodeStyle() {
        return [
          {
            type: go.Panel.Spot,
            layerName: "Background",
            locationObjectName: "SHAPE",
            selectionObjectName: "SHAPE",
            locationSpot: go.Spot.Center
          },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        ];
      }

      function shapeStyle() {
        return {
          name: "SHAPE",
          stroke: "black",
          fill: "#f0f0f0",
          portId: "", // So a link can be dragged from the Node: see /GraphObject.html#portId
          fromLinkable: true,
          toLinkable: true
        };
      }

      function textStyle() {
        return [
          {
            font: "bold 11pt helvetica, bold arial, sans-serif",
            margin: 2,
            editable: true
          },
          new go.Binding("text", "label").makeTwoWay()
        ];
      }

      // Node templates
      myDiagram.nodeTemplateMap.add("stock",
        $(go.Node, nodeStyle(),
          $(go.Shape, shapeStyle(),
            { desiredSize: new go.Size(50, 30) }),
          $(go.TextBlock, textStyle(),
            {
              _isNodeLabel: true,  // declare draggable by NodeLabelDraggingTool
              alignment: new go.Spot(0.5, 0.5, 0, 30)    // initial value
            },
            new go.Binding("alignment", "label_offset", go.Spot.parse).makeTwoWay(go.Spot.stringify))
        ));

      myDiagram.nodeTemplateMap.add("cloud",
        $(go.Node, nodeStyle(),
          $(go.Shape, shapeStyle(),
            {
              figure: "Cloud",
              desiredSize: new go.Size(35, 35)
            })
        ));

      myDiagram.nodeTemplateMap.add("valve",
        $(go.Node, nodeStyle(),
          {
            movable: false,
            layerName: "Foreground",
            alignmentFocus: go.Spot.None
          },
          $(go.Shape, shapeStyle(),
            {
              figure: "Ellipse",
              desiredSize: new go.Size(20, 20)
            }),
          $(go.TextBlock, textStyle(),
            {
              _isNodeLabel: true,  // declare draggable by NodeLabelDraggingTool
              alignment: new go.Spot(0.5, 0.5, 0, 20)    // initial value
            },
            new go.Binding("alignment", "label_offset", go.Spot.parse).makeTwoWay(go.Spot.stringify))
        ));

      myDiagram.nodeTemplateMap.add("variable",
        $(go.Node, nodeStyle(),
          { type: go.Panel.Auto },
          $(go.TextBlock, textStyle(),
            { isMultiline: false }),
          $(go.Shape, shapeStyle(),
            // the port is in front and transparent, even though it goes around the text;
            // in "link" mode will support drawing a new link
            { isPanelMain: true, stroke: null, fill: "transparent" })
        ));

      // Link templates
      myDiagram.linkTemplateMap.add("flow",
        $(go.Link,
          { toShortLength: 8 },
          $(go.Shape,
            { stroke: "blue", strokeWidth: 5 }),
          $(go.Shape,
            {
              fill: "blue",
              stroke: null,
              toArrow: "Standard",
              scale: 2.5
            })
        ));

      myDiagram.linkTemplateMap.add("influence",
        $(go.Link,
          { curve: go.Link.Bezier, toShortLength: 8 },
          $(go.Shape,
            { stroke: "green", strokeWidth: 1.5 }),
          $(go.Shape,
            {
              fill: "green",
              stroke: null,
              toArrow: "Standard",
              scale: 1.5
            })
        ));
    }

    function setMode(mode, itemType) {
      myDiagram.startTransaction();
      document.getElementById(SD.itemType + "_button").className = SD.mode + "_normal";
      document.getElementById(itemType + "_button").className = mode + "_selected";
      SD.mode = mode;
      SD.itemType = itemType;
      if (mode === "pointer") {
        myDiagram.allowLink = false;
        myDiagram.nodes.each(function(n) { n.port.cursor = ""; });
      } else if (mode === "node") {
        myDiagram.allowLink = false;
        myDiagram.nodes.each(function(n) { n.port.cursor = ""; });
      } else if (mode === "link") {
        myDiagram.allowLink = true;
        myDiagram.nodes.each(function(n) { n.port.cursor = "pointer"; });
      }
      myDiagram.commitTransaction("mode changed");
    }

    // Show the diagram's model in JSON format that the user may edit
    function save() {
      document.getElementById("mySavedModel").value = myDiagram.model.toJson();
      myDiagram.isModified = false;
    }
    function load() {
      myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
    }
  </script>
</head>
<body onload="init()">
  <div id="sample">
    <h3>GoJS System Dynamics</h3>
    <div id="myDiagram" style="width:600px; height:500px; border:solid 1px black"></div>
    <button id="pointer_button" class="pointer_selected" onclick="setMode('pointer','pointer');">Pointer</button>
    <button id="stock_button" class="node_normal" onclick="setMode('node','stock');" style="margin-left:20px;">Stock</button>
    <button id="cloud_button" class="node_normal" onclick="setMode('node','cloud');">Cloud</button>
    <button id="variable_button" class="node_normal" onclick="setMode('node','variable');">Variable</button>
    <button id="flow_button" class="link_normal" onclick="setMode('link','flow');" style="margin-left:20px;">Flow</button>
    <button id="influence_button" class="link_normal" onclick="setMode('link','influence');">Influence</button>
    <p>
      A <em>system dynamics diagram</em> shows the storages (stocks) and flows of material in some system,
      and the factors that influence the rates of flow.
      It is usually a cosmetic interface for building mathematical models --
      you provide values and equations for the stocks and flows,
      and appropriate software can then simulate the system's behaiour.
    </p>
    <p>
      The diagram has two types of link: flow links and influence links.
      In additon to the node attached to each flow, there are 3 types of node:
      <ul>
        <li><b>stocks</b>, the amount of some substance</li>
        <li><b>clouds</b>, like stocks, but outside the system of interest</li>
        <li><b>variables</b>, either numeric constants or calculated from other elements</li>
      </ul>
    </p>
    <p>
      The conventional user interface for building system dynamics diagrams is modal --
      you select a tool in the toolbar, then either click in an empty part of the diagram to add a node
      or drag from one node to another to add a link.
      That is the approach used in this example, accomplished with the <a>clickCreatingTool</a> and <a>linkingTool</a>.
      Note that you need to click on the Pointer tool to revert to the normal mode.
    </p>
    <p>
      In addition to the above, the diagram also installs the <a href="../extensions/NodeLabelDragging.html">NodeLabelDraggingTool</a>
      extension into <a>ToolManager.mouseMoveTools</a>.
    </p>
    <p>
      This sample is based on a prototype developed by Robert Muetzelfeldt.
    </p>
    <div>
      <div>
        <button id="SaveButton" onclick="save()">Save</button>
        <button onclick="load()">Load</button>
        Diagram Model saved in JSON format:
      </div>
      <textarea id="mySavedModel" style="width:100%; height:400px">
{ "class": "go.GraphLinksModel",
  "linkLabelKeysProperty": "labelKeys",
  "nodeDataArray": [
{"key":"grass", "category":"stock", "label":"Grass", "loc":"30 220", "label_offset":"0.5 0.5 0 30"},
{"key":"cloud1", "category":"cloud", "loc":"200 220"},
{"key":"sheep", "category":"stock", "label":"Sheep", "loc":"30 20","label_offset":"0.5 0.5 0 -30"},
{"key":"cloud2", "category":"cloud", "loc":"200 20"},
{"key":"cloud3", "category":"cloud", "loc":"-150 220"},
{"key":"grass_loss", "category":"valve", "label":"grass_loss","label_offset":"0.5 0.5 0 20" },
{"key":"grazing", "category":"valve", "label":"grazing","label_offset":"0.5 0.5 45 0" },
{"key":"growth", "category":"valve", "label":"growth","label_offset":"0.5 0.5 0 20" },
{"key":"sheep_loss", "category":"valve",  "label":"sheep_loss","label_offset":"0.5 0.5 0 20" },
{"key":"k1", "category":"variable",  "label":"good weather", "loc": "-80 100"},
{"key":"k2", "category":"variable",  "label":"bad weather", "loc": "100 150"},
{"key":"k3", "category":"variable",  "label":"wolves", "loc": "150 -40"}
  ],
  "linkDataArray": [
{"from":"grass", "to":"cloud1", "category":"flow", "labelKeys":[ "grass_loss" ]},
{"from":"sheep", "to":"cloud2", "category":"flow", "labelKeys":[ "sheep_loss" ]},
{"from":"grass", "to":"sheep", "category":"flow", "labelKeys":[ "grazing" ]},
{"from":"cloud3", "to":"grass", "category":"flow", "labelKeys":[ "growth" ]},
{"from":"grass", "to":"grass_loss", "category":"influence"},
{"from":"sheep", "to":"sheep_loss", "category":"influence"},
{"from":"grass", "to":"growth", "category":"influence"},
{"from":"grass", "to":"grazing", "category":"influence"},
{"from":"sheep", "to":"grazing", "category":"influence"},
{"from":"k1", "to":"growth", "category":"influence"},
{"from":"k1", "to":"grazing", "category":"influence"},
{"from":"k2", "to":"grass_loss", "category":"influence"},
{"from":"k3", "to":"sheep_loss", "category":"influence"}
  ]
}
      </textarea>
    </div>
  </div>
</body>
</html>
